use clap::{Args, Parser, Subcommand};
use std::{collections::HashMap, path::PathBuf};

mod data_box;
mod error;

use data_box::DataBox;
use error::Error;

#[derive(Args)]
struct Config {
    /// Default name of keys if not specified.
    #[clap(long)]
    default_key: Option<String>,

    /// Directory to save bodies and keys.
    #[clap(long)]
    data_dir: Option<PathBuf>,
}

/// Password manager based on asymmetric encryption and git repository.
///
/// Read output of `ezpass tutor` for more help.
#[derive(Parser)]
struct CommandLine {
    #[clap(flatten)]
    config: Config,

    #[clap(subcommand)]
    task: Option<Task>,
}

#[derive(Subcommand)]
enum Task {
    /// Create a pair of secret & public key with name.
    GenerateKey {
        name: String,
    },

    Tutor,

    /// Set, get or remove config flags.
    Config {
        name: String,
        value: Option<String>,

        /// Remove config instead of set or get it.
        #[clap(short, long)]
        remove: bool,
    },

    /// List configs.
    ConfigList,

    /// Pull and push data box with remote git server.
    Sync {
        /// Remote name of data box as a git repo, default be "origin".
        remote: Option<String>,
    },

    /// Tell secrets to others.
    Speech {
        /// Secret key.
        #[clap(short, long)]
        key: Option<String>,
    },

    /// Insert a named password.
    ///
    /// Name is a short description to your password, and specified in command line, while body of message is always
    /// read from standard input.
    Insert {
        /// Name to your secret.
        name: String,

        /// Overwrite existing secret.
        #[clap(short, long)]
        force: bool,
    },

    /// Show body of secret with a name.
    Show {
        /// Secret key.
        #[clap(short, long)]
        key: Option<String>,

        /// Name of secret.
        name: String,

        /// Treat body of message as TOTP secret to show TOTP token.
        #[clap(short, long)]
        totp: bool,
    },

    /// List name of secrets.
    List {
        /// Secret key.
        #[clap(short, long)]
        key: Option<String>,
    },
}

fn main() {
    if runtime().is_err() {
        std::process::exit(1);
    }
}

fn load_flags() -> Result<HashMap<String, String>, Error> {
    let flagfile = flags_path()?;

    if !flagfile.is_file() {
        return Ok(HashMap::default());
    }

    let content = std::fs::read_to_string(flagfile)?;
    let flags: HashMap<String, String> = toml::from_str(&content)?;

    Ok(flags)
}

fn flags_path() -> Result<PathBuf, Error> {
    let mut flagfile = std::env::current_exe()?;
    flagfile.set_extension("toml");
    Ok(flagfile)
}

fn runtime() -> Result<(), Error> {
    let mut flags = load_flags()?;

    let cli = CommandLine::parse_from(
        std::iter::once("ezpass".to_string())
            .chain(flags.iter().map(|(k, v)| format!("--{k}={v}")))
            .chain(std::env::args().skip(1)),
    );

    let data_box = DataBox::new(cli.config);

    match cli.task.unwrap_or(Task::List { key: None }) {
        Task::Tutor => {
            print!("{}", include_str!("tutor.txt"));
        }
        Task::Config {
            name,
            value,
            remove,
        } => {
            if remove {
                if let Some(value) = flags.remove(&name) {
                    eprintln!("Remove config {name} = {value}");
                } else {
                    eprintln!("Can't find config {}", name);
                    return Err(Error);
                }
            } else if let Some(value) = value {
                flags.insert(name, value);
                let flags_string = toml::to_string_pretty(&flags)?;
                std::fs::write(flags_path()?, flags_string)?;
            } else if let Some(value) = flags.get(&name) {
                println!("{}", value);
            } else {
                eprintln!("Can't find config {}", name);
                return Err(Error);
            }
        }
        Task::ConfigList => {
            for (k, v) in flags.iter() {
                println!("{} = {}", k, v);
            }
        }

        Task::GenerateKey { name } => data_box.generate_key(name)?,
        Task::Sync { remote } => data_box.sync(remote)?,

        Task::List { key } => data_box.list(key)?,
        Task::Speech { key } => data_box.speech(key)?,
        Task::Insert { name, force } => data_box.insert(name, force)?,
        Task::Show { key, name, totp } => data_box.show(key, name, totp)?,
    }

    Ok(())
}
